Skip to content
On this page

复杂数据处理 · 结构转换(上)


第 9 节 复杂数据处理 · 结构转换(上)

前面我们相继介绍了多种数据结构,它们各自都承担着不同类型数据的承载功能。不同的数据之间有着不同的表现方式,而在实际工作中我们却常常需要将不同的数据类型进行相互转换,以满足不同的需求。

9.1 Any ↔ 字符串

在开发数据应用的时候,有大部分的数据都不会是由 JavaScript 或用户的操作实时生成的,更多的是直接从服务端的数据存储设施中提取出来,然后通过网络协议传输到客户端以用于展示。

这样的话我们可以首先引入一个题外话,既然我们知道前端使用的数据大部分都需要通过网络协议从服务端传往前端,那这样一个传输过程就是抽象内容的编码和解编码的过程。而且在计算机科学中,通信协议基本上都是以字符串(或二进制)为基础承载数据结构,也就是说在一个服务端与客户端的通信架构中,会需要将各种数据结构首先转换为字符串,经过了网络的传输过程而达到另一端之后,再以相同的方式转换为原本的数据结构。

network-transport

9.1.1 JSON

JSON,全称为 JavaScript Object Notation,是目前最流行的网络数据传输格式之一。相比于 CSV(Comma-Separated Values,逗号分隔值)、XML(Extensible Markup Language,可扩展标记语言)等历史更为悠久的格式化数据传输格式,JSON 同时拥有着易读性强(完全符合 JavaScript 标准)、格式不敏感和轻量化的特点。

{
  "name": "Chaoyang Gan",
  "nickname": "iwillwen"
}

JSON 是一个 JavaScript 语言标准的子集,它完全可以直接运行在 JavaScript 引擎中。当然因为 JavaScript 语言本身是具有可被攻击的可能性的,所以在解析 JSON 数据内容的时候,并不能直接作为一段 JavaScript 代码运行。

JavaScript 引擎中提供了一个 eval 函数以用于运行一段 JavaScript 代码,所以假如一段 JSON 数据内容是绝对安全的,确实可以使用 eval 函数当做是 JSON 解析器。

const jsonStr = `{
  "name": "Chaoyang Gan",
  "nickname": "iwillwen"
}`

eval('var me = ' + jsonStr)

console.log(me.name) //=> Chaoyang Gan

但如果需要解析的 JSON 数据并不能保证安全甚至可以被恶意篡改(通过中间人劫持、XSS 攻击等方式),就会出现非常不安全的情况,严重者会导致用户私密信息被盗取。

const somethingImportant = 'some secret'

const jsonStr = `{
  "attack": (function(){
    alert(somethingImportant)
  })()
}`

eval('var me = ' + jsonStr) //=> some secret

为了避免这种情况的出现,我们必须使用现代 JavaScript 引擎中提供的或其他可信任的 JSON.parse 函数进行解码和 JSON.stringify 函数进行编码。

JSON.parse(`{
  "attack": (function(){
    alert(somethingImportant)
  })()
}`) //=> SyntaxError: Unexpected token ( in JSON

言归正传,通常来说,我们可以把将非字符串类型的数据通过某种算法转换为字符串的过程称为序列化(字符串也是一种有序序列),而利用 JSON 格式便是目前最流行的序列化方法之一。

const jsonStr = JSON.stringify({
  name: 'Chaoyang Gan',
  nickname: 'iwillwen'
})

console.log(jsonStr) //=> {"name":"Chaoyang Gan","nickname":"iwillwen"}

9.1.2 直接转换

JSON 格式的好处是将结构不确定的数据转换为字符串格式,但同时也会强行带来可能不必要的内容,比如 JSON 的边界字符(如 "{} 等)。在需要转换的目标数据类型是确定的,而且将序列化后的字符串数据进行解析的接收方也是可控的的情况下,可以选择直接对数据进行类型转换。

数值类型

在 JavaScript 中所有的对象都会默认带有一个 toString 方法,而对于数值类型来说,可以直接使用这个方法来进行向字符串类型的转换。

const n1 = 1
const n2 = 1.2

const s1 = n1.toString()
const s2 = n2.toString()

console.log(s1, typeof s1) //=> 1 string
console.log(s2, typeof s2) //=> 1.2 string

除了将数值直接转换为字符串之外,我们常常需要实现一个将数据类型的小数点后的值固定在一个长度范围内,比如 5 \-> 5.003.1415 \-> 3.14,这个主要用于表格和图表的展示上。3.1415 可以通过数值计算得到需要的 3.14,但是 5 没办法直接通过计算得到 5.00。因为 JavaScript 并不像其他语言那样区分开整型和非整型的数值,所以它提供了一个用于实现这个需求的方法 Number.toFixed。这个方法接受一个数值参数,即小数点后的保留位数,一般来说这个参数需要是非负整型数值,当然如果传入一个非整型数值,该方法也会自动取整进行计算。

const int = 5
const pi = Math.PI //=> 3.141592653589793 (约等于)

console.log(int.toFixed(2)) //=> '5.00'
console.log(pi.toFixed(2)) //=> '3.14'
console.log(int.toFixed(pi)) //=> '5.000'

转换成字符串之后还可以通过 parseIntparseFloat 将以字符串形式存储的数值转换为整型或浮点型。

console.log(parseInt('5.00')) //=> 5
console.log(parseFloat('3.14')) //=> 3.14

布尔型(逻辑型)

布尔型也就是(幸亏 JavaScript 并不存在中间态),在 JavaScript 中表现为 truefalse。显而易见,这两个值各自都有一个以英文单词来表示的意义,那么我们自然可以非常简单地对其进行转换了。

console.log(true.toString()) //=> 'true'
console.log(false.toString()) //=> 'false'

但是要将其再转换成布尔型就没那么简单了,因为 JavaScript 中并没有直接提供 parseBoolean 这样的函数,而且作为弱类型语言,JavaScript 在进行一些判断时也有不少让人非常费解的“操作”。

true == 'true' //=> false
false == 'false' //=> false

true == 1 //=> true
false == 0 //=> true

所以一般来说我们可以使用强类型判断 === 分别判断一个字符串是否是 "true",不是则为 false

function parseBoolean(string) {
  return string === 'true'
}

console.log(parseBoolean('true')) //=> true
console.log(parseBoolean('false')) //=> false

数组

事实上,我们在第 2 节中就已经接触过字符串中的 split 方法,它用于将一个字符串以指定字符串为分隔符分割成一个数组。

const str = '1,2,3,4,5'
const arr = str.split(',')

console.log(arr) //=> [ 1, 2, 3, 4, 5 ]

对应地,数组也可以进行组合变成一个字符串,使用的是 Array.join 方法。

const arr = [ 1, 2, 3, 4, 5 ]

console.log(arr.join()) //=> 1,2,3,4,5
console.log(arr.join('#')) //=> 1#2#3#4#5

9.2 对象 ↔ 数组

我们在第 5 节中介绍对象字面量的时候曾经介绍过,在 JavaScript 中的数组实际上是一个特殊的对象字面量,那么在从属关系上看数组应该是对象字面量的一个子集 Array ubseteq Object

但为什么我们这里还是要提到对象和数组之间的互相转换呢?假设我们需要将一个对象字面量中的属性以列表的形式展示出来:

object-as-list

虽然各种框架都有相关的函数或者工具来完成这个需求,但是为了更好地理解数据结构之间的差异及对其的应用,我们还是需要了解其中如何进行数据格式的转换。

JavaScript 中提供了一个 Object.keys() 函数,可以提取出对象的所有属性键,并以数组的形式表示。

const object = {
  "name": "Chaoyang Gan",
  "title": "Engineer",
  "subject": "Maths"
}

const keys = Object.keys(object)
console.log(keys) //=> ["name", "title", "subject"]

得到了目标对象的属性键数组后,配合数组的 .map 方法便可以将每一个属性键对应的值提取出来。

const list = keys.map(key => {
  return {
    key, value: object[key]
  }
})

console.log(list)
//=> [
// {key: "name", value: "Chaoyang Gan"},
// {key: "title", value: "Engineer"},
// {key: "subject", value: "Maths"}
// ]

当然我们可以将第二层中的对象也使用数组表示。

const pairs = keys.map(key => {
  return [ key, object[key] ]
})

console.log(pairs)
// => [
// ["name", "Chaoyang Gan"],
// ["title", "Engineer"],
// ["subject", "Maths"]
// ]

同样,我们也可以使用 Lodash 中提供的 _.toPairs 方法将对象转换为以双元素为键值对表达方式的数组。

const pairs = _.toPairs(object)

完成了从对象到数组的转换后自然需要一个将其进行逆转换的方法,可以直接使用 Lodash 中提供的 _.fromPairs

const object = _.fromPairs(pairs)
console.log(object)
// => {
// name: "Chaoyang Gan",
// title: "Engineer",
// subject: "Maths"
// }

事实上,我们在第 5 节中用过的 _.groupBy 函数也是一种将数组转换为对象的方法,但它更多的是为了将数组根据其中的某一个字段或某一种变换结果来进行字典化,而不是单纯地将其进行转换。

我们需要明确的原则是,数据转换的出发点和目的都是为了服务需求,而不是单纯地将其进行数据结构上的转换,在思考如何对数据进行处理之前,首先要明确目标需求究竟需要怎样的数据形式。 究竟是需要一个以数值作为元素的数组(如人工神经网络的输入和输出值),还是以对象作为元素类型的数组以用于表格的展示(每一个对象元素代表表格中的一行),或是以列为单位存储的数据框对象(如 ECharts 框架中常用)。

// Input data for ANN
const xorArray = [ 1, 0, 0, 1, 1, 0, 1 ]

// Row-base dataset
const rDataset = [
  { name: "iwillwen", gender: "male" },
  { name: "rrrruu", gender: "female" }
]

// Column-base dataset
const cDataset = {
  name: [ "iwillwen", "rrrruu" ],
  gender: [ "male", "female" ]
}

小结

我们在这一节中学习了字符串、对象以及数组间的相互转化,这些都是比较常见也比较简单的数据转换需求和方法,一般用于数据的预处理和使用过程中的转换步骤。

习题

  1. 我们分别介绍了两种可以存储一个对象信息的数组格式,请分别实现它们的逆转换过程 fromList(用于以 { key: "key", value: "value" } 为元素的数组)和 fromPairs
  2. 请分别实现 Row-base dataset 和 Column-base dataset 之间的转换过程。